Skip to content

feat(ui): persistent timeline cache with stale-while-revalidate#427

Merged
matt2e merged 9 commits intomainfrom
cache-commits-for-remote
Mar 20, 2026
Merged

feat(ui): persistent timeline cache with stale-while-revalidate#427
matt2e merged 9 commits intomainfrom
cache-commits-for-remote

Conversation

@matt2e
Copy link
Contributor

@matt2e matt2e commented Mar 20, 2026

Summary

  • Add persistent timeline caching with a stale-while-revalidate pattern: cached data is shown immediately while fresh data is fetched in the background
  • Display a compact "Looking for changes..." revalidating row with slide animation while background refresh is in progress
  • Invalidate timeline caches when projects or branches are deleted to prevent stale data

Test plan

  • Verify timeline loads instantly from cache on revisit, with revalidating indicator appearing briefly
  • Confirm fresh data replaces stale data once the background fetch completes
  • Check that deleting a branch or project properly invalidates the relevant timeline caches
  • Verify the revalidating row styling is subtle (grey, compact) and animates in/out smoothly

🤖 Generated with Claude Code

matt2e and others added 8 commits March 20, 2026 15:02
Remove the 30s TTL from the timeline cache so cached data never expires
on its own. When switching back to a project, show cached timeline data
immediately and display a "Looking for changes..." spinner row at the
bottom while revalidating in the background.

- Remove TIMELINE_CACHE_MAX_AGE_MS; cache entries persist until
  explicitly invalidated
- Add getBranchTimelineWithRevalidation() that returns cached data
  and kicks off a background re-fetch
- Add invalidateProjectBranchTimelines() for batch cache eviction
- Update BranchCard.loadTimeline() to use stale-while-revalidate
  on initial load with a version counter to discard stale results
- Add revalidating prop to BranchTimeline with a pending-commit
  style "Looking for changes..." row
- Invalidate cache entries on project and branch deletion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Set loading=false in cached path so timeline data isn't hidden behind
  the loading spinner when switching back to a project
- Add !revalidating to all isLast calculations so the vertical connector
  line extends to the "Looking for changes..." row
- Move cache invalidation to success path in branch deletion to avoid
  triggering revalidation for a just-deleted branch
- Remove dead fetchedAt field from timeline cache entries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a 'revalidating' type to TimelineRow that renders a grey spinner
(default icon styling) instead of green commit styling. The row always
hides the vertical connector line and uses reduced vertical padding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the icon box background/border and use --text-faint for the
spinner, icon, and title text so the "Looking for changes..." row
blends into the background rather than competing with actual timeline
content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keep the icon background box but apply opacity: 0.5 so it fades into
the background rather than disappearing entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace opacity hack with --bg-hover for the icon box background and
border, making it barely visible against the page background while
still present.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bump compact row padding from 4px to 6px vertical to give the
"Looking for changes..." row a bit more breathing room.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wrap the "Looking for changes..." row in a slide transition so it
smoothly animates in and out, matching the other dynamic timeline rows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matt2e matt2e requested review from baxen and wesbillman as code owners March 20, 2026 05:27
When background revalidation fails and we have cached timeline data,
keep showing the stale data with a subtle error below instead of
replacing it with a full error state. Also cancel in-flight
revalidations when loadTimeline is called from any path to prevent
stale responses from overwriting fresher data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a1d7ae2f0b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +309 to +311
.catch((e) => {
if (version !== revalidationVersion) return;
error = e instanceof Error ? e.message : String(e);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep stale timeline visible when background refresh fails

In the stale-while-revalidate path, a transient failure now assigns error even though timeline still holds the cached data. Because this component renders {:else if error} before {:else if timeline} later in BranchCard.svelte, opening a previously cached branch while offline (or during any backend hiccup) replaces the usable stale timeline with an error panel instead of preserving the fallback content.

Useful? React with 👍 / 👎.

Comment on lines +364 to +366
return {
cached: entry?.timeline ?? null,
fresh: getBranchTimeline(branchId, { force: true }),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid reusing mount-time refresh for later forced timeline loads

getBranchTimelineWithRevalidation() starts a forced fetch immediately, and getBranchTimeline() still deduplicates by branch ID against any in-flight request. That means if the user starts a note/commit while this initial refresh is still running, the later loadTimeline() calls that are meant to show the new pending stub immediately (see the session-status-changed handler in BranchCard.svelte) collapse onto the older request that was issued before the new session existed. On slower timelines, the card can therefore stay stuck on the pre-action state until some later refresh happens.

Useful? React with 👍 / 👎.

@matt2e matt2e merged commit 14f8fb8 into main Mar 20, 2026
4 checks passed
@matt2e matt2e deleted the cache-commits-for-remote branch March 20, 2026 05:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant